﻿//-----------------------------------------------------------------------
// <copyright file="TaskDescription.cs" company="Numbrella foundation">
// Copyright (c) Numbrella foundation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace Numbrella.Core
{
	using System;
	using System.Collections.Generic;
	using System.Collections.ObjectModel;
	using System.Diagnostics;

	/// <summary>
	/// Represents holder for information about a task of a problem.
	/// </summary>
	/// <typeparam name="TResultContents">Type of the task result contents.</typeparam>
	internal class TaskDescription<TResultContents>
	{
		/// <summary>
		/// Contains unique identifier of the associated task.
		/// </summary>
		private readonly int taskId;

		/// <summary>
		/// Contains a value indicating how much times each task should be evaluated for validation.
		/// </summary>
		private readonly int taskEvaluationCount;

		/// <summary>
		/// Contains list of all task solve attempts.
		/// </summary>
		private readonly List<TaskSolveAttempt<TResultContents>> solveAttempts = new List<TaskSolveAttempt<TResultContents>>();

		/// <summary>
		/// Contains list of task solve attempts considered as valid.
		/// </summary>
		private List<TaskSolveAttempt<TResultContents>> validSolveAttempts;

		/// <summary>
		/// Contains task status.
		/// </summary>
		private TaskStatus status;

		/// <summary>
		/// Contains approximate amount of results required to accomplish validation.
		/// </summary>
		private int requiredResultsApproximateAmount;

		/// <summary>
		/// Initializes a new instance of the <see cref="TaskDescription{TResultContents}"/> class.
		/// </summary>
		/// <param name="taskId">Unique identifier of the associated task.</param>
		/// <param name="taskEvaluationCount">Value indicating how much times each task should be evaluated for validation.</param>
		public TaskDescription(int taskId, int taskEvaluationCount)
		{
			Debug.Assert(taskId >= 0, "Task identifier should not be negative.");
			Debug.Assert(taskEvaluationCount >= 1, "Task evaluation count should be positive.");

			this.taskId = taskId;
			this.taskEvaluationCount = taskEvaluationCount;
			this.requiredResultsApproximateAmount = taskEvaluationCount;
			this.status = TaskStatus.Pending;
		}

		/// <summary>
		/// Gets collection of all valid task solve attempts.
		/// </summary>
		public ReadOnlyCollection<TaskSolveAttempt<TResultContents>> ValidSolveAttempts
		{
			get
			{
				Debug.Assert(this.status == TaskStatus.Completed, "Task should be completed before accessing this property.");
				return this.validSolveAttempts.AsReadOnly();
			}
		}

		/// <summary>
		/// Gets collection of all task solve attempts.
		/// </summary>
		public ReadOnlyCollection<TaskSolveAttempt<TResultContents>> SolveAttempts
		{
			get { return this.solveAttempts.AsReadOnly(); }
		}

		/// <summary>
		/// Gets approximate amount of result required to accomplish validation.
		/// </summary>
		public int RequiredResultsApproximateAmount
		{
			get { return this.requiredResultsApproximateAmount; }
		}

		/// <summary>
		/// Gets task status.
		/// </summary>
		public TaskStatus Status
		{
			get { return this.status; }
		}

		/// <summary>
		/// Finds solve attempt with timeout exceeded and removes them from <see cref="SolveAttempts"/> collection if found.
		/// </summary>
		/// <param name="timeout">Task solve timeout.</param>
		public void UpdateTimedOutSolveAttempts(TimeSpan timeout)
		{
			List<TaskSolveAttempt<TResultContents>> attemptsToRemove = new List<TaskSolveAttempt<TResultContents>>();
			foreach (TaskSolveAttempt<TResultContents> attempt in this.solveAttempts)
				if (attempt.ReceivedResult == null && attempt.IsTimeoutExceeded(timeout))
					attemptsToRemove.Add(attempt);

			this.solveAttempts.RemoveAll(attemptsToRemove.Contains);
		}

		/// <summary>
		/// Checks if current task is waiting for result from user with given name.
		/// That means that user has taken current task before.
		/// </summary>
		/// <param name="userName">User name.</param>
		/// <returns>True if current task is waiting for result from user with given name; otherwise, false.</returns>
		public bool IsWaitingForResultFromUser(string userName)
		{
			TaskSolveAttempt<TResultContents> taskSolveAttempt = this.solveAttempts.Find(s => s.UserName == userName);
			return taskSolveAttempt != null && taskSolveAttempt.ReceivedResult == null;
		}

		/// <summary>
		/// Checks if user with the given name can take current task.
		/// User can participate in solving each task of a problem only once
		/// (to prevent ability of posterior validation mechanism hacking).
		/// </summary>
		/// <param name="userName">User name.</param>
		/// <returns>True if user with the given name can take task; otherwise, false.</returns>
		public bool CanUserTakeTask(string userName)
		{
			return this.solveAttempts.Find(s => s.UserName == userName) == null;
		}

		/// <summary>
		/// Gets a value indicating whether task requires more solvers to accomplish validation.
		/// </summary>
		/// <returns>True if task requires more solvers; otherwise, false.</returns>
		public bool GetIfMoreSolversAreRequired()
		{
			int solvingNow = this.solveAttempts.FindAll(s => s.ReceivedResult == null).Count;
			return solvingNow < this.requiredResultsApproximateAmount;
		}

		/// <summary>
		/// Gets a value indicating whether task requires more solvers to accomplish validation.
		/// If task solve attempt timeout was exceeded, task is considered as being in need for some solvers.
		/// </summary>
		/// <param name="timeout">Task solve timeout.</param>
		/// <returns>True if task requires more solvers; otherwise, false.</returns>
		public bool GetIfMoreSolversAreRequired(TimeSpan timeout)
		{
			if (this.GetIfMoreSolversAreRequired())
				return true;
			return this.solveAttempts.FindAll(s => s.ReceivedResult == null && s.IsTimeoutExceeded(timeout)).Count > 0;
		}

		/// <summary>
		/// Notifies that specified user wants to take a task.
		/// </summary>
		/// <param name="userName">User name.</param>
		public void TakeTask(string userName)
		{
			Debug.Assert(!String.IsNullOrEmpty(userName), "User name can not be null or empty.");
			Debug.Assert(this.status != TaskStatus.Completed, "Task should not be completed when taking task.");
			Debug.Assert(this.GetIfMoreSolversAreRequired(), "Task should require more solvers when taking task.");
            Debug.Assert(this.CanUserTakeTask(userName), "User should be able to take this task.");
			
			this.status = TaskStatus.Processing;
			this.solveAttempts.Add(new TaskSolveAttempt<TResultContents>(userName, DateTime.Now));
		}

		/// <summary>
		/// Submits result and checks if validation can be accomplished.
		/// </summary>
		/// <param name="taskResult">Submited result.</param>
		/// <param name="userName">User name.</param>
		public void SubmitResult(TaskResult<TResultContents> taskResult, string userName)
		{
			Debug.Assert(!String.IsNullOrEmpty(userName), "User name can not be null or empty.");
			Debug.Assert(taskResult != null, "Task result can not be null");
			Debug.Assert(this.status == TaskStatus.Processing, "Task should be in 'Processing' status when receiving result.");
			Debug.Assert(taskResult.TaskId == this.taskId, "Task result should have correct task identifier.");

			// Find task solver in active solvers queue
			TaskSolveAttempt<TResultContents> attempt = this.solveAttempts.Find(s => s.UserName == userName);
			Debug.Assert(attempt != null, "Solver must exist.");

			// Notify that result was received
			attempt.ReceiveResult(taskResult);

			// Try to validate
			this.Validate();
		}

		/// <summary>
		/// Validates result set.
		/// </summary>
		private void Validate()
		{
			Debug.Assert(this.status != TaskStatus.Completed, "Task already completed can not be validated again.");

			this.requiredResultsApproximateAmount = this.taskEvaluationCount;

			foreach (TaskSolveAttempt<TResultContents> attempt1 in this.solveAttempts)
			{
				if (attempt1.ReceivedResult == null)
					continue;
				
				int equalResultsCount = 1;
				List<string> userNames = new List<string> { attempt1.UserName };

				foreach (TaskSolveAttempt<TResultContents> attempt2 in this.solveAttempts)
				{
					if (attempt2.ReceivedResult == null || attempt2 == attempt1)
						continue;

					Debug.Assert(attempt1.ReceivedResult.TaskId == attempt2.ReceivedResult.TaskId, "All the results in the result set should have equal identifiers.");

					bool areResultsEqual = Equals(attempt1.ReceivedResult.Contents, attempt2.ReceivedResult.Contents);
					if (!areResultsEqual)
						continue;

					equalResultsCount += 1;
					userNames.Add(attempt2.UserName);
				}

				if (equalResultsCount == this.taskEvaluationCount)
				{
					this.status = TaskStatus.Completed;
					this.validSolveAttempts = this.solveAttempts.FindAll(s => userNames.Contains(s.UserName));
				}

				this.requiredResultsApproximateAmount = Math.Min(
					this.requiredResultsApproximateAmount,
					this.taskEvaluationCount - equalResultsCount);
			}
		}
	}
}
